KURZUS: Programozás alapjai

MODUL: III. modul

9. lecke: Mutatók 1/2

| Ebben a leckében elkezdünk megismerkedni a mutatókkal, azok tulajdonságaival, alkalmazási szabályaikkal és lehetséges felhasználási körükkel. A C nyelv erősen épít a mutatókra: sok feladat ugyan megoldható nélkülük is, de bizonyos esetekben hatékonyabb, egyszerűbb vagy kifejezőbb megoldás adható egy problémára, de olyan esetet is mutatni fogunk, ami egyáltalán nem lenne kezelhető mutatók nélkül. A mutatókkal hatékony eszközt kap kezébe a programozó, de egyben veszélyeset is: sok nehezen felfedezhető hiba a mutatók helytelen alkalmazására vezethető vissza.
A lecke végére a hallgatónak tisztában kell lennie
A mutatók fő típusaival. Egyszerű mutatódeklarációk elkészítésének módjával. A címképző és indirekció operátorok helyes alkalmazásával. A különböző típusú mutatók között elvégezhető típusmódosítások lehetőségeivel és korlátaival. A konstans mutatókkal kapcsolatos ismeretekkel. A mutatók függvényparaméterként történő alkalmazásában rejlő lehetőségekkel ("cím szerinti" paraméterátadás). A mutatók és a tömbök közötti kapcsolat részleteivel. A mutatóaritmetika szabályaival. Mutatók közötti relációk használatával. Karaktermutatók használatával, a legfontosabb könyvtári karakterlánc kezelő függvényekkel. Szükség esetén képesnek kell lennie egyszerű karakterlánc manipuláló függvények készítésére mutatók alkalmazásával. A mutatótömbökre vonatkozó ismeretekkel.
A lecke kulcsfogalmai: mutató, adatmutató, kódmutató, NULL és void mutató, konstans és konstanst címző mutató, címképzés, indirekció, cím szerinti paraméterátadás, index operátor, nem teljes típusú tömb, tárillesztés, mutatótömb.
A mutatók fő típusai
A nyelvben a mutatóknak két fajtája van:
(adat) objektumot címző mutatók (adatmutatók) és függvényt (kódot) címző mutatók (kódmutatók).
Bármilyenek is legyenek, a mutatók memória címek tárolására szolgálnak. Előbb az adatmutatókkal fogunk foglalkozni. A mutató mindaddig, míg erről külön nem szólunk, jelentsen adatmutatót!
A típus típusú mutató típus típusú objektum címét tartalmazhatja. A mutatók unsigned egészek, de saját szabályaik és korlátozásaik vannak a hozzárendelésre, a konverzióra és az aritmetikára.
Miután a mutató is skalár objektum, létezik a mutatót címző mutató is. A mutatók azonban többnyire más skalár vagy void objektumokra (változókra), ill. aggregátumokra (tömbökre, struktúrákra és uniókra) mutatnak.
Mutatódeklarációk
A mutatódeklaráció elnevezi a mutató változót, és rögzíti annak az objektumnak a típusát, melyre ez a változó mutathat.
Tehát csak egy bizonyos típusra (előredefiniáltra - beleértve a void-ot is - vagy felhasználó definiáltára) mutató mutató deklarálható
típus *azonosító;
módon, amikor is az azonosító nevű mutató típus típusú objektum címét veheti fel.
Vegyük észre, hogy típus * az azonosító típusa! Például az
    int *imut;
    int *fv(char *);
deklarációk alapján: imut int típusú objektumot címző mutató, fv int típusú címet visszaadó, és egy char objektumra mutató paramétert fogadó függvény.
Ha a definiált mutató statikus élettartamú, akkor tiszta zérus kezdőértéket kap implicit módon. A fordító tiszta zérus címen nem helyez el sem objektumot, sem függvényt, s így a zérus cím (az ún. NULL mutató) speciális felhasználású. Magát a NULL mutatót a szabványos fejfájlokban (például az stdio.h- ban) definiálja a nyelv.
Bármilyen típusú mutató összehasonlítható a NULL értékkel, ill. bármilyen típusú mutatóhoz hozzárendelhetjük a NULL mutatót.
Ha lokális a definiált mutató, akkor a definíció hatására a fordító akkora memóriaterületet foglal, hogy abban egy cím elférjen, de a lefoglalt bájtok "szemetet" tartalmaznak. Az ilyen mutató tehát nem használható mindaddig "értelmesen" semmire, míg valamilyen módon érvényes címet nem teszünk bele. Hogy lehet elérni valamilyen objektum címét?
Cím operátor (&)
Egyoperandusos, magas prioritású művelet, mely definíció szerint:
& előtag-kifejezés
alakú. Az előtag-kifejezés operandusnak vagy függvényt kell kijelölnie, vagy olyan objektumot elérő balértéknek kell lennie, mely nem register tárolási osztályú, és nem bitmező. Lehet tehát például változó, vagy tömbelem. (A bitmezőkkel a struktúrák kapcsán később foglalkozunk!)
Ha az operandus típusa típus, akkor az eredmény mutató a típus típusra. Cím csak mutatónak adható át. A lehetséges módszerek szerint vagy hozzárendeljük, vagy inicializátort alkalmazunk a deklarációban. Például:
    típus val1, val2, *ptr = &val1; /* A ptr mutatónak van
      kezdőértéke, de a vele elért objektumnak nincs. */
    ptr = &val2; /* Hozzárendeljük a másik változó
                    címét a mutatóhoz. */
Ha
    T1 *ptr1; T2 *ptr2;
különböző típusú objektumokat címző mutatók, akkor a
    ptr1 = ptr2;
vagy a
    ptr2 = ptr1;
hozzárendelés figyelmeztető vagy hibaüzenetet okoz. Explicit típusmódosító szerkezetet alkalmazva viszont "gond nélkül" mehet a dolog:
    ptr1 = (T1 *) ptr2;
    ptr2 = (T2 *) ptr1;
Teljesen illegális dolog azonban a függvény és az adatmutatók összerendelése!
Ha a mutató már érvényes címet tartalmaz, az
Indirekció operátor (*)
segítségével elérhetjük a mutatott értéket. Az indirekció operátor ugyancsak egyoperandusos, magas prioritású művelet, melynek definíciója:
* előtag-kifejezés
Az előtag-kifejezés operandusnak típus típust címző mutatónak kell lennie, ahol a típus bármilyen lehet. Az indirekció eredménye az előtag-kifejezés mutatta címen levő, típus típusú érték. Az indirekció eredménye egyben balérték is. Például:
    típus t1, t2;
    /* . . . */
    típus *ptr = &t1; /* A mutatót inicializáltuk is. */
    *ptr = t2;        /* Ekvivalens a t1 = t2-vel. */
Ne kíséreljük meg azonban a cím operátorral kifejezés vagy konstans címét előállítani, vagy indirekció operátort nem címmé kiértékelhető operandus elé odaírni,
    ptr = &(t1 + 6); /* HIBÁS */
    ptr = &8;        /* HIBÁS */
    t2 = *t1;        /* HIBÁS */
mert hibaüzenethez jutunk!
Meghatározatlan az indirekció eredménye akkor is, ha a mutató:
NULL mutató, vagy olyan lokális objektum címét tartalmazza, mely a program adott pontján nem látható, vagy a végrehajtható program által nem használható címet tartalmaz.
A mutatókkal végzett munka során "ököl-szabályunk" szerint: ahol objektum lehet egy kifejezésben, ott kerek zárójelbe téve az indirekció műveletét követően ilyen típusú objektumot címző mutató is állhat. Ha:
    típus val, *ptr = &val;
akkor ahol val szerepelhet egy kifejezésben, ott állhat (*ptr) is.
Például:
    int y, val=0, *ptr = &val;
    /* . . . */
    printf("val címe (%p) van ptr-ben (%p).\n", &val, ptr);
    y = *ptr + 3; /* y = val +3 tulajdonképpen. */
    y = sqrt((double)*ptr); /* y = sqrt(val). */
    *ptr += 6; /* val += 6. */
    (*ptr)++; /* Zárójel nélkül a ptr-t inkrementáltatnánk. */
    printf("val értéke %d.\n", val);
    printf("A ptr címen levő érték %d.\n", *ptr); }
Fedezzük fel, hogy a cím, vagy mutatótartalom kijelzéséhez használatos típuskarakter a p a formátumspecifikációban, és hogy a printf paraméterlistájában cím értékű kifejezés is állhat, persze akár indirekcióval is!
Vigyázzunk nagyon! Ha egy változónak nem adunk kezdőértéket, és mondjuk, hozzáadogatjuk egy tömb elemeit, akkor a végeredmény "zöldség", s a dolog szarvashiba. Ha mutatónak nem adunk kezdőértéket, és a benne levő "szemétre", mint címre, írunk ki értéket indirekcióval, akkor az duplán szarvashiba. A "szeméttel", mint címmel valahol "pancsolunk" a memóriában, és felülírjuk valami egészen más objektum értékét, s a hiba is egészen más helyen jelentkezik, mint ahol elkövettük. Korszerű operációs rendszerek megvédik ugyan a többi program adat- és kódterületét a hibásan működő programtól oly módon, hogy azonnal leállítják a rossz programot, de a program saját adatterületének nem kívánt módosításától semmiféle operációs rendszer és mechanizmus nem véd meg.
void mutató
Külön kell említenünk a
    void *vptr;
mutató típust, ami nem semmire, hanem meghatározatlan típust címző mutató. Explicit típusmódosító szerkezet alkalmazása nélkül bármilyen típusú mutató vagy cím hozzárendelhető a void mutatóhoz. A dolog megfordítva is igaz, azaz bármilyen típusú mutatóhoz hozzárendelhetünk void mutatót. Például:
    típus ertek, *ptr=&ertek;
    vptr=ptr;    /* OK */
    vptr=&ertek; /* OK */
    ptr=vptr;    /* OK */
void mutatóval egyetlen művelet nem végezhető csak: az indirekció, hisz meghatározatlan a típus, és a fordító nem tudja, hogy hány bájtot és milyen értelmezésben kell elérnie.
    ertek=*vptr; /* HIBÁS */
Statikus és lokális címek
Miután a statikus élettartamú objektum minden bitjét zérusra inicializálja alapértelmezés szerint a fordító, de címe nem változik, kezdőértéke lehet akár statikus mutatónak is. Az auto változók címe viszont nem lehet statikus inicializátor, hisz a cím más-más lehet a blokk különböző végrehajtásakor.
    int GLOBALIS;
    int fv(void){
      int LOKALIS;
      static int *slp = &LOKALIS;   /* HIBÁS. */
      static int *sgp = &GLOBALIS;  /* OK. */
      register int *rlp = &LOKALIS; /* OK. */
      long a = 1000000l, *lp = &a;  /* a kezdőértéke
               1000000 és lp kezdetben rá mutat. */
      register int *pi = 0; /* pi regiszteres mutató
                               értéke NULL. */
      const int i = 26; /* Ez az egyetlen hely, ahol
                           i értéket kaphatott. */
      /* . . . */ }
Mutatódeklarátorok
deklarátor: <mutató>direkt-deklarátor
direkt-deklarátor: azonosító (deklarátor) direkt-deklarátor [<konstans-kifejezés>] direkt-deklarátor (paraméter-típus-lista) direkt-deklarátor (<azonosítólista>)
mutató: *<típusmódosító-lista> *<típusmódosító-lista>mutató
típusmódosító-lista: típusmódosító típusmódosító-lista típusmódosító
típusmódosító: (a következők egyike!) const volatile
A deklarátorok szerkezetileg az indirekcióhoz, a függvényhez és a tömbkifejezéshez hasonlítanak, s csoportosításuk is azonos. A deklarátort követheti egyenlőségjel után inicializátor, de mindig deklaráció-specifikátorok előzik meg. A deklaráció-specifikátorok tárolási-osztály-specifikátor és típusspecifikátor sorozat tulajdonképp, és igazából nem csak egyetlen deklarátorra vonatkoznak, hanem ezek listájára.
A direkt-deklarátor definíció első lehetősége szerint a deklarátor egy egyedi azonosítót deklarál, melyre a tárolási osztály egy az egyben vonatkozik, de a típus értelmezése kicsit függhet a deklarátor alakjától is. A deklarátor tehát egyedi azonosítót határoz meg, s mikor az azonosító feltűnik egy tőle típusban nem eltérő kifejezésben, akkor a vele elnevezett objektum értékét eredményezi.
Összesítve, és csak a lényeget tekintve a deklaráció
típus deklarátor
alakú. Ezt nem változtatja meg az sem, ha a direkt-deklarátor második alternatíváját tekintjük, mert a zárójelezés nem módosítja a típust, csak összetettebb deklarátorok kötésére lehet hatással. (A függvénydeklarátorokat már tárgyaltuk, s a tömbdeklarátorokra még ebben a leckében visszatérünk!)
A mutatódeklaráció így módosul:
típus * <típusmódosító-lista> deklarátor
ahol a * <típusmódosító-listá>-val változtatott típus a deklarátor típusa. A * operátor után álló típusmódosító magára a mutatóra, és nem a vele megcímezhető objektumra vonatkozik.
Foglalkozzunk például a const módosítóval!
Konstans mutató
Mind a mutató, mind a mutatott objektum deklarálható const-nak. Bármely const-nak deklarált "valami" ugyebár nem változtathatja meg az értékét. Az sem mehet persze, hogy olyan mutatót kreáljunk, mellyel megsérthetnénk a const objektum érték megváltoztathatatlanságát.
    int i;
    int *pi; /* pi int objektumot címző
                inicializálatlan mutató. */
    int * const cp = &i; /* cp konstans mutató int-re, de
                            amire mutat, az nem konstans. */
    const int ci = 7; /* ci 7 értékű konstans int. */
    const int *pci; /* pci konstans int-re mutat. A
                       mutató tehát nem konstans. */
    const int * const cpc = &ci; /* cpc konstans int-re
                                    mutató konstans mutató. */
A következő hozzárendelések legálisak:
    i = ci; /* const int hozzárendelése int-hez. */
    *cp = ci; /* const int hozzárendelése konstans mutató
                 mutatta nem konstans int-hez. */
    ++pci; /* const int-re mutató mutató inkrementálása.*/
    pci = cpc; /* Konstans int-re mutató, konstans mutató hozzárendelése
                  const int-re mutató mutatóhoz.*/
A következő hozzárendelések illegálisak:
    ci = 0; /* Értékhozzárendelési kísérlet const int-hez.*/
    ci--; /* const int dekrementálási kísérlete. */
    *pci = 3; /* Értékadási kísérlet a const int-re mutató
                 mutatóval megcímzett objektumnak. */
    cp = &ci; /* Értékadási kísérlet konstans mutatónak. */
    cpc++; /* Konstans mutató inkrementálási kísérlete. */
    pi = pci; /* Ha ez a hozzárendelés legális lenne, akkor
                 *pi = ... módon módosíthatnánk azt a const
                 int-et, amire pci mutat. */
Ahhoz, hogy a "kígyó megharapja a farkát" kell, hogy const objektumot címző mutatót ne lehessen hozzárendelni nem const-ra mutató mutatóhoz. Ha ez menne, akkor ugyan "kerülő úton", de a mutatott const érték megváltoztatható lenne.
Mutatók és függvényparaméterek
Eddig kiemelten csak az ún. érték szerinti paraméter átadással foglalkoztunk a függvényhívás kapcsán. Ezt úgy interpretálhatjuk, hogy a fordító függvényhíváskor az aktuális paraméterek (esetleg típuskonverzión átesett) értékét helyezi el, például a veremben, s a meghívott függvény nem az aktuális paraméterek értékéhez, hanem annak csak egy másolatához fér hozzá.
Rövidsége és találósága miatt átvesszük [5] vonatkozó mintapéldáját! Tegyük fel, hogy a programozó azt a feladatot kapta, hogy írjon olyan függvényt, mely megcseréli két, int paramétere értékét!
    csere(paraméter1, paraméter2);
módon hívható első kísérlete a következő volt:
    void csere(int x, int y){
      int seged = x;
      x = y;
      y = seged; }
Barátunk próbálkozása "professzionális" olyan értelemben, hogy gondolt arra, hogy a csere végrehajtásához szüksége van segédváltozóra, de a függvényt hívó programjában "meglepetten" tapasztalta, hogy semmiféle értékcsere nem történt.
A csere tulajdonképpen lezajlott az aktuális paraméterek másolatain a veremben, de ennek semmilyen hatása sincs az aktuális paraméterek hívó programbeli értékeire. A függvény visszatérése miatt a veremmutató is visszaállt a hívás előtti értékére, s így a verembeli, felcserélt értékmásolatok is elérhetetlenné váltak.
A megoldás a cím szerinti paraméter átadásban rejlik, azaz a csere függvényt
csere(&paraméter1, &paraméter2);
módon kell meghívni, s a függvénydefiníció pedig így módosul:
    void csere(int *x, int *y){
      int seged = *x;
      *x = *y;
      *y = seged; }
Valójában tehát nincsen szintaktikai lehetőség az "igazi" cím szerinti paraméter átadásra a nyelvben, de a címek másolatainak érték szerinti átadásával, majd a függvényen belüli indirekcióval meg lehet kerülni a problémát. Így a hívott rutin képes módosítani a hívó függvényben látható változókat, hiszen azok címeinek másolata is ugyanazt a változót jelöli ki a tárban.
Tömbök és mutatók
Tömb létesíthető aritmetikai típusokból, de definiálható
mutatóból (külön későbbi fejezet), struktúrából és unióból (későbbi lecke), valamint tömbből (következő lecke).
Bármilyen típusból is hozzuk azonban létre a(z egydimenziós) tömböt, a típusnak teljesnek kell lennie. Nem lehet félig kész, nem teljesen definiált, felhasználói típusból tömböt kreálni.
A tömbök és a mutatók között nagyon szoros kapcsolat van. Ha kifejezés, vagy annak része típus tömbje, akkor a (rész)kifejezés értékét a tömb első elemét megcímző, konstans mutatóvá alakítja a fordító, és a (rész)kifejezés típusa típus * const lesz. Nem hajtja végre a fordító ezt a konverziót, ha a (rész)kifejezés cím operátor (&), ++, --, vagy a pont (.) szelekciós operátor, vagy a sizeof operandusa, vagy hozzárendelési művelet bal oldalán áll. (A . operátorral majd a struktúráknál foglalkozunk.)
Elemezzük az előző bekezdésben mondottakat egy példa tükrében! Legyen a következő tömb és mutató!
    #define MERET 20 /* Tömbméret. */
    /* . . . */
    float tomb[MERET], *pt;
    /* . . . */
A tömböt a fordító, mondjuk, a 100-as címtől kezdve helyezte el a memóriában. Egy tömbelem helyfoglalása sizeof(tomb[0]) ? sizeof(float) ? 4, általánosságban sizeof(típus). Az elemek tomb[0], tomb[1], ..., tomb[MERET - 1] sorrendben, növekvő címeken helyezkednek el a tárban. Tehát a tomb[0] a 100-as, a tomb[1] a 104-es, a tomb[2] a 108-as és így tovább címen van. A 180-as memóriacím már nem tartozik a tömbhöz.
Ha valamilyen kifejezésben meglátja a fordító a tomb azonosítót, akkor rögtön helyettesíteni fogja a float * const 100-as címmel. Tehát, ha a pt-t fel kívánjuk tölteni tomb kezdőcímével, akkor nem kell ilyen
    pt = &tomb[0];
hosszadalmasan kódolni, tökéletesen elég a
    pt = tomb;
Index operátor
A Műveletek és kifejezések lecke elején definiált utótag-kifejezés második alternatívája az
utótag-kifejezés[kifejezés]
az indexelő operátor. Nevezik ezt indexes változónak is, vagyis mindenképpen hivatkozás ez a tömb egy meghatározott elemére.
A szabályok szerint az utótag-kifejezés és a kifejezés közül az egyiknek mutatónak, a másiknak egész típusúnak kell lennie, és hatásukra a fordító a
*((utótag-kifejezés) + (kifejezés))
műveletet valósítja meg. A fordító alkalmazza a következő fejezetben tárgyalt konverziós szabályokat a + műveletre és a tömbre. Ha az utótag-kifejezés a tömb és a kifejezés az egész típusú, akkor a konstrukció a tömb kifejezésedik elemére hivatkozik.
Mi van a típussal? A külső zárójel párban még mutató típus (típus *) van, s ezen hajtja végre a fordító az indirekciót. Tehát az eredmény típusát a mutató dönti el.
Vegyük a tomb[6] indexes változót! A mondottak szerint ebből
    *((tomb) + (6))
lesz, ami nem 6, hanem 6-nak, mint indexnek, a hozzáadását jelenti a tomb kezdőcíméhez, azaz:
    *((float *)100 + 6*sizeof(float))
Az összeadás elvégzése után
    *((float *)124)
ami az indirekció végrehajtása után a tomb 6-os indexű elemét eredményezi.
A + kommutatív művelet, így az indexelés is az. Egydimenziós tömbökre a következő négy kifejezés teljesen ekvivalens feltéve, hogy p mutató és i egész:
    p[i]  i[p]  *(p + i) *(i + p)
Folytassuk az index operátor ismertetésének megkezdése előtt elkezdett gondolatmenetet, azaz a pt mutató legyen tomb értékű!
    pt = tomb;
Ilyenkor:
    *pt  tomb[0]
    *(pt + 1)  tomb[1]
és általában
    *(pt + i)  tomb[i]
Vegyük észre, hogy a pt-re szükség sincs a tömbelemek és címeik előállításához, hisz:
    tomb[i]  *(tomb + i)
és
    &tomb[i]  tomb + i.
Fontos különbség van azonban a tomb és a pt között. A pt mutató változó. A tomb pedig mutató konstans. Ebből következőleg nem megengedettek a következők:
    tomb = pt;  /* Mintha 3=i-t írtunk volna fel. */
    tomb++;     /* Mint 3++. */
    pt = &tomb; /* Mintha &3-at akarnánk előállítani. */
A mutató változóra természetesen megengedettek ezek a műveletek:
    pt = tomb;
    pt++;
Ha pt mutató, akkor azt kifejezés indexelheti a tanultak szerint, azaz
    pt[i]  *(pt + i)
Meg kell említeni még, hogy amikor egy függvényt tömbazonosító aktuális paraméterrel hívtunk meg, akkor is cím szerinti paraméter átadás történt, azaz a függvény a tömb kezdőcím konstans másolatát kapta meg, például, a veremben. Ez a címmásolat aztán persze a függvényben már nem konstans, el is lehet rontani stb.
Vegyük észre, hogy a cím szerinti paraméter átadást szinte minden példánkban, már a kezdetek óta használjuk! Eddig a függvényparaméter tömböt mindig
típus azonosító[]
alakban adtuk meg, de legújabb ismereteink szerint a
típus * azonosító
forma használandó, hisz ez egyértelműen mutatja, hogy a paraméter mutató. A függvény testén belül ettől függetlenül szabadon dönthet a programozó, hogy
tömbként, mutatóként, vagy vegyesen
kezeli az ilyen paramétert.
Írjuk át ennek szellemében a pelda21.c datume függvényét!
    int datume(const char *s) {
      static int honap[ ] =
        { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
      int i, ho;
      if(!*(s+10) && !isdigit(*(s+4)) && *(s+4)==*(s+7)) {
        for(i=0; i<10; ++i) {
          if(i==4||i==7) ++i;
          if(!isdigit(*(s+i))) return 0; }
        if((i=atoi(s))>=1) {
          *(honap+2)=28+(!(i%4)&&i%100 || !(i%400));
          if((ho=atoi(&s[5])>=1&&ho<=12&&
             (i=atoi(&s[8])>=1&&i<=*(honap+ho)) return 1; } }
      return 0; }
Lássuk be, hogy az ilyen fajta függvény átírásnak, amikor s[i]-ből *(s+i)-t csinálgatunk, semmi értelme sincs, hisz ezt a fordító magától is megteszi. Az átalakítás egyetlen előnye, hogy a formális paraméter jobban szemlélteti, hogy const karakterláncra mutat, ill. az észre vehető, hogy az atoi függvényt nem csak a karaktertömb kezdetével szabad meghívni.
Foglalkozzunk még egy kicsit a tömbdeklarátorokkal!
Tömbdeklarátor és nem teljes típusú tömb
A Mutatódeklarátorok fejezetben a direkt-deklarátor definíció harmadik változata
direkt-deklarátor [<konstans-kifejezés>]
a tömbdeklarátor. A tömbdeklaráció tehát
típus deklarátor [<konstans-kifejezés>]<={inicializátorlista<,>}>
ahol az elhagyható konstans-kifejezésnek egész típusúnak és zérusnál nagyobb értékűnek kell lennie, s ez a tömb mérete. Ez a Típusok és konstansok lecke Deklaráció fejezetében írottakon túl további korlátozásokat ró a konstans kifejezésre, hogy egész típusúnak kell lennie. Operandusai ebben az esetben csak egész, felsorolás, karakteres és lebegőpontos állandók lehetnek, de a lebegőpontos konstanst explicit típuskonverzióval egésszé kell alakítani. Operandus lehet még a sizeof operátor is, aminek operandusára természetesen nincsenek ilyen korlátozások.
Tudjuk, hogyha elmarad a tömbméret, akkor a fordító az inicializátorlista elemszámának megállapításával rögzíti azt, s a típus így válik teljessé. Megadott méretű tömb esetében a lista inicializátorainak száma nem haladhatja meg a tömb elemeinek számát. Ha az inicializátorok kevesebben vannak a tömbméretnél, akkor a magasabb indexű, fennmaradó tömbelemek zérus kezdőértéket kapnak. Tudjuk azt is, hogy tömb inicializátorai csak állandó kifejezések lehetnek.
Ne felejtkezzünk meg róla, hogy ugyan a karaktertömb inicializátorlistája karakterlánc konstans, de az előbb felsorolt korlátozások ugyanúgy vonatkoznak rá is! Vagyis rögzített méretű tömb esetén a karakterlánc hossza sem haladhatja meg a méretet. Ha a lánchossz és a tömbméret egyezik, nem lesz lánczáró zérus a karaktertömb végén.
Ha a tömbdeklarációból hiányzik a tömbméret és inicializátorlista sincs, akkor a deklaráció nem teljes típusú tömböt határoz meg. Lássuk a lehetséges eseteket!
Ez csak előzetes deklaráció és a tömb méretét majd egy későbbi definíció rögzíti. Például: float t[]; /* . . . */ float t[20]; Ez csak előzetes deklaráció és a tömbméretet egy későbbi inicializátorlistás deklaráció tisztázza. Például: char lanc[]; /* . . . */ char lanc[]="Ne tudd ki!"; Természetesen az együtt tömbméretes és inicializátorlistás definiáló deklaráció is megengedett! A globális tömböt nem ebben a forrásmodulban definiálták, itt csak egyszerűen a rá való hivatkozás előtt deklarálták. Például: extern double dtomb[]; Persze a dtomb méretét valamilyen módon ebben a forrásmodulban is ismernünk kell! A tömb függvény paramétere. Például: void fv(float t[], int n) { /* . . . */ } A tömb méretét valahonnan a függvényben is tudni kéne! Például úgy, hogy további paraméterként átadják neki.
Mutatóaritmetika és konverzió
A mutató vagy címaritmetika az inkrementálásra, a dekrementálásra, az összeadásra, a kivonásra és az összehasonlításra szorítkozik. A típus típusú objektumot megcímző mutatón végrehajtott aritmetikai műveletek automatikusan figyelembe veszik a típus méretét, azaz az objektum tárolására elhasznált bájtok számát. A mutatóaritmetika ezen kívül feltételezi, hogy a mutató a típus típusú objektumok tömbjére mutat, azaz például:
    int i = 6;
    float ftomb[50], *fptr = ftomb;
esetén
    fptr += i;
hatására az fptr-beli cím
    sizeof(float)*i
-vel (általánosságban sizeof(típus)*egész-szel) nő, azaz a példa szerint ftomb[6]-ra mutat.
Ha ptr1 a típus típusú tömb második és ptr2 a tizedik elemére mutat, akkor a két mutató különbsége
    ptr2 - ptr1 -> 8.
Figyeljük meg, hogy a mutatókhoz indexértéket adunk hozzá vagy vonunk ki belőle és a mutatók különbsége is indexérték! Igazából a két mutató különbsége ptrdiff_t típusú egész indexkülönbség. A ptrdiff_t az stddef.h fejfájlban definiált, s többnyire signed int.
Összeadás, kivonás, inkrementálás és dekrementálás
Összeadás egyik operandusa lehet mutató, ha a másik operandus egész típusú. Az eredmény ilyenkor a címaritmetika szabályait követő mutató, azaz olyan cím, mely egész*sizeof(típus)-sal nagyobb, mint az eredeti. A típus a mutató által mutatott nem void típus. Kivonásnál az első operandus lehet valamilyen objektumra mutató mutató, ha ilyenkor a második egész típusú. Az eredmény most is a mutatóaritmetika szabályait követő mutató, azaz olyan cím, mely egész*sizeof(típus)-sal kisebb, mint az eredeti. A típus a mutató által mutatott nem void típus. Kivonásnál mindkét operandus lehet ugyanazon objektum részeire mutató mutató. Az eredmény az előző két pont szellemében a mutatóaritmetika szabályainak megfelelő egész (indexkülönbség). Az egész típusa a szabvány stddef.h fejfájlban definiált ptrdiff_t (signed int).
Ha az operandus mutató, akkor az eggyel növelésben (++) vagy csökkentésben (--) a címaritmetika szabályai érvényesek, azaz az eredmény mutató a következő, vagy a megelőző elemre fog mutatni.
Ha p mutató a tömb utolsó elemére mutat, akkor a ++p még legális érték: a tömb utolsó utáni elemének címe, de minden ezután következő mutatónövelés definiálatlan eredményre vezet. Hasonló probléma van akkor is, ha p a tömb kezdetére mutat. Ilyenkor a mutatócsökkentés - már a --p is - definiálatlan eredményt okoz.
Minden nem tömbre alkalmazott címaritmetikai művelet eredménye definiálatlan. Ugyanez mondható el akkor is, ha a tömbre alkalmazott mutatóaritmetikai művelet eredménye a tömb legelső eleme elé, vagy a legutolsó utánin túlra mutat.
Relációk
A következő operandus típuskombinációk használhatók relációkban:
Mindkét operandus lehet ugyanazon típusú objektumot címző mutató, amikor is a két objektum memória címének összehasonlításáról lesz szó. A mutató összehasonlítás csak egyazon objektum részeire definiált, és a mutatók nem tartalmazhatnak ezen objektumon kívülre irányuló címet sem egyetlen kivétellel, tömb esetén megengedett az utolsó létező elem utáni címére való hivatkozás. Ha a mutatók tömb elemekre hivatkoznak, akkor az összehasonlítás az indexek összehasonlításával ekvivalens. A nagyobb indexű elem címe magasabb. Az egyenlőségi relációkat használva a mutató hasonlítható a konstans 0 értékhez, azaz a NULL mutatóhoz is. Csak az egyenlőségi relációk esetében a mutató hasonlítható a konstans, 0 értékű, egész kifejezéshez, vagy void-ra mutató mutatóhoz. Ha mindkét mutató NULL mutató, akkor egyenlők.
Feltételes kifejezés
Ha a K1 ? K2 : K3-ban K2, K3 egyike-másika mutató, akkor a K2 vagy K3 operandusok típusától függő konstrukció eredményének típusa a következő:
Ha az egyik operandus valamilyen típusú objektumot címző mutató és a másik void mutató, akkor az eredmény (esetleges konverzió után) ugyancsak void mutató. Ha az egyik operandus mutató, s a másik operandus 0 értékű konstans kifejezés, akkor az eredmény típusa a mutató típusa lesz.
Mutatók típusának összehasonlításakor a const vagy volatile típusmódosítók nem szignifikánsak, de az eredmény típusa megörökli mindkét oldal módosítóit.
Írjuk át a címaritmetika alkalmazásával a pelda20.c- ben használt dverem.c- t!
    /* DVEREMUT.C: double verem push, pop és clear
                   fuggvenyekkel. */
    #define MERET 128       /* A verem merete. */
    static double v[MERET]; /* A verem. */
    static double *vmut=v;  /* A veremmutato. */
    int clear(void) {
      vmut=v;
      return MERET; }
    double push(double x) {
      if(vmut<v+MERET) return *vmut++=x;
      else return x+1.; }
    double pop(void) {
      if(vmut>v) return *--vmut;
      else return 0.; }
A vmut most valóban veremmutató a double veremben (és nem a processzoréban). v kezdőértékkel indul, és mindig a következő szabad helyre irányul. Ha kisebb, mint a veremhez már nem tartozó cím (v+MERET), akkor a push kiteszi rá a paraméterét, és mellékhatásként előbbre is lépteti eggyel a veremmutatót a következő szabad helyre. A pop csak akkor olvas a veremből, ha van benne valami (vmut>v). Kiszedéskor előbb vissza kell állítani a veremmutatót (az előtag --), s csak aztán érhető el indirekcióval a legutoljára kitett érték, s most ez lesz a következő szabad hely is egyben a következő push számára.
Konverzió
A fordító által automatikusan elvégzett, implicit konverziókat már megismertük a címaritmetika műveleteinél.
Az explicit típuskonverziós (típusnév) előtag-kifejezés
szerkezetben a (típusnév) többnyire (típus *) alakú lesz, és ilyen típusú objektumra mutató mutatóvá konvertálja az előtag-kifejezés értékét. Például:
    char *lanc;
    int *ip;
    /* . . . */
    lanc = (char *) ip;
Nullaértékű konstans, egész kifejezés, vagy ilyen (void *)-gal típusmódosítva konvertálható explicit típusmódosítással, hozzárendeléssel vagy összehasonlítással akármilyen típusú mutatóvá. Ez NULL mutatót eredményez, mely megegyezik az ugyanilyen típusú NULL mutatóval, de eltér bármely más objektumra vagy függvényre mutató mutatótól.
Egy bizonyos típusú mutató konvertálható más típusú mutatóvá. Az eredmény azonban címzés hibás lehet, ha nem megfelelő tárillesztésű objektumot érne el. Csak azonos, vagy kisebb szigorúságú tárillesztési feltételekkel bíró adattípus mutatójává konvertálható az adott mutató, és onnét vissza.
A tárillesztés hardver sajátosság, s azt jelenti, hogy a processzor bizonyos típusú adatokat csak bizonyos határon levő címeken helyezhet el. A legkevésbé megszorító a char típus szokott lenni. A short csak szóhatáron (2-vel maradék nélkül osztható címen) kezdődhet, a long viszont dupla szóhatáron (4-gyel maradék nélkül osztható címen) helyezkedhet el, s így tovább.
void mutató készíthető akármilyen típusú mutatóból, és megfordítva korlátozás és információvesztés nélkül. Ha az eredményt visszakonvertáljuk az eredeti típusra, akkor az eredeti mutatót állítjuk újra elő.
Ha ugyanolyan, de más, const vagy volatile módosítójú típusra konvertálunk, akkor az eredmény ugyanaz a mutató a módosító által előidézett megszorításokkal. Ha a módosítót aztán elhagyjuk, akkor a további műveletek során az eredetileg az objektum deklarációjában szereplő const vagy volatile módosítók maradnak érvényben.
A mutató mindig konvertálható a tárolásához elegendően nagy, egész típussá. A mutató mérete, és az átalakító függvény persze nem gépfüggetlen. Leírunk egy, több fejlesztő rendszerben is használatos mutató-egész és egész-mutató konverziót.
A mutató-egész konverzió módszere függ a mutató és az egész típus méretétől, valamint a következő szabályoktól:
Ha a mutató mérete nem kisebb az egész típusénál, akkor a mutató érték unsigned-ként viselkedik azzal a megkötéssel, hogy nem konvertálható lebegőpontossá. Ha a mutató mérete kisebb az egész típusénál, akkor a mutatót előbb az egésszel megegyező méretűvé alakítja a fordító, s aztán konvertálja egésszé.
Az egész-mutató átalakítás sem portábilis, de a következő szabályok szerint mehet például:
Ha az egész típus ugyanolyan méretű, mint a mutató, akkor az egész értéket unsigned-ként mutatónak tekinti a fordító. Ha az egész típus mérete különbözik a mutatóétól, akkor az egész típust az előző pontokban ismertetett módon konvertálja előbb mutató méretű, egész típusúvá, majd unsigned-ként mutatónak tekinti.
Karaktermutatók
Tudjuk, hogy a karakterlánc konstans karaktertömb típusú, s ebből következőleg mögötte ugyancsak egy cím konstans van, hisz például a
    printf("Ez egy karakterlánc konstans.");
kitűnően működik, holott a printf függvény első paramétere const char * típusú. Ez a konstans mutató azonban a tömbtől eltérően nem rendelkezik azonosítóval sem, tehát később nincs módunk hivatkozni rá. Ennek elkerülésére, azaz a cím konstans értékének megőrzésére, a következő módszerek ajánlhatók:
    char *uzenet;
    uzenet = "Kész a kávé!\n";
vagy
    const char *uzenet = "Kész a kávé!\n";
Karakterlánc kezelő függvények
A rutinok prototípusai a szabványos string.h fejfájlban helyezkednek el. Egyik részüknek str-rel, másik csoportjuknak mem-mel kezdődik a neve. Az str kezdetűek karakterláncokkal (char *), míg a mem nevűek memóriaterületekkel bájtonként haladva (void * és nincs feltétlenül lánczáró zérus a bájtsorozat végén) foglalkoznak. A char *, vagy void * visszaadott értékű függvények mindig az eredmény lánc kezdőcímét szolgáltatják.
A memmove-tól eltekintve, a többi rutin viselkedése definiálatlan, ha egymást a memóriában átfedő karaktertömbökre használják őket. Néhányat - teljességre való törekvés nélkül - felsorolunk közülük!
char *strcat(char *cel, const char *forras); char *strncat(char *cel, const char *forras, size_t n);
A függvények a cel karakterlánchoz fűzik a forras-t (strcat), vagy a forras legfeljebb első, n karakterét (strncat), és visszatérnek az egyesített cel karakterlánc címével. Nincs hibát jelző visszaadott érték! Nincs túlcsordulás vizsgálat a karakterláncok másolásakor és hozzáfűzésekor.
A size_t többnyire az unsigned int típusneve.
Írjuk csak meg a saját strncat függvényünket!
    char *strncat(char *cel, const char *forras, size_t n){
      char *seged = cel;
      /* Pozícionálás a cel lezáró '\0' karakterére: */
      while(*cel ) ++cel;
      /* forras cel végére másolása a záró '\0'-ig, vagy
         legfeljebb n karakterig: */
      while(n-- && (*cel++ = *forras++));
      /* Vissza az egyesített karakterlánc kezdőcíme: */
      return seged; }
char *strchr(const char *string, int c); void *memchr(const void *string, int c, size_t n);
A rutinok a c karaktert keresik string-ben, ill. string első n bájtjában, és az első előfordulás címével térnek vissza, ill. NULL mutatóval, ha nincs is c a string-ben, vagy az első n bájtjában. A lánczáró zérus is lehet c paraméter. Az
char *strrchr(const char *string, int c);
ugyanazt teszi, mint az strchr, csak c string-beli utolsó előfordulásának címével tér vissza, ill. NULL mutatóval, ha nincs is c a string-ben.
int strcmp(const char *string1, const char *string2); int strncmp(const char *string1, const char *string2, size_t n); int memcmp(const void *string1, const void *string2, size_t n);
A függvények unsigned char típusú tömbökként összehasonlítják string1 és string2 karakterláncokat, és negatív értéket szolgáltatnak, ha string1 < string2. Pozitív érték jön, ha string1 > string2. Az egyenlőséget viszont a visszaadott zérus jelzi.
Az strncmp és a memcmp a hasonlítást legföljebb az első n karakterig, ill. bájtig végzik.
A legtöbb fejlesztő rendszerben nem szabványos stricmp és strnicmp is szokott lenni, melyek nem kis-nagybetű érzékenyen hasonlítják össze a karakterláncokat.
A saját strcmp:
    int strcmp(const char *s1, const char *s2 ){
      for( ; *s1 == *s2; ++s1, ++s2)
        if(!(*s1)) return 0; /* s1 == s2 */
      return(*s1 - *s2); } /* s1 < s2 vagy s1 > s2 */
char *strcpy(char *cel, const char *forras); char *strncpy(char *cel, const char *forras, size_t n); void *memcpy(void *cel, const void *forras, size_t n); void *memmove(void *cel, const void *forras, size_t n);
Az strcpy a forras karakterláncot másolja lánczáró karakterével együtt a cel karaktertömbbe, és visszatér a cel címmel. Nincs hibát jelző visszatérési érték. Nincs túlcsordulás ellenőrzés a karakterláncok másolásánál.
Az strncpy a forras legfeljebb első n karakterét másolja. Ha a forras n karakternél rövidebb, akkor cel végét '\0'-ázza n hosszig a rutin. Ha az n nem kisebb, mint a forras mérete, akkor nincs zérus a másolt karakterlánc végén.
A memcpy és a memmove mindenképpen n bájtot másolnak. Egyetlenként a karakterlánc kezelő függvények közül, a memmove akkor is biztosítja az átlapoló memóriaterületeken levő, eredeti forras bájtok felülírás előtti átmásolását, ha a forras és a cel átfedné egymást.
A saját strcpy:
    char *strcpy(char *cel, const char *forras ){
      char *seged = cel;
      /* forras cel-ba másolása lezáró '\0' karakterével: */
      while(*cel++ = *forras++);
      /* Vissza a másolat karakterlánc kezdőcíme: */
      return seged; }
size_t strlen(const char *string);
A rutin a string karakterlánc karaktereinek számával tér vissza a lánczárót nem beszámítva. Nincs hibát jelző visszaadott érték!
char *strpbrk(const char *string, const char *strCharSet);
A függvény az strCharSet-beli karakterek valamelyikét keresik a string-ben, és visszaadják az első előfordulás címét, ill. NULL mutatót, ha a két paraméternek közös karaktere sincs. A keresésbe nem értendők bele a lánczáró zérusok.
A példában számokat keresünk az s karakterláncban.
    #include <string.h>
    #include <stdio.h>
    void main(void){
      char s[100] = "3 férfi és 2 fiu 5 disznót ettek.\n";
      char *er = s;
      int i=1;
      while(er = strpbrk(er, "0123456789"))
        printf("%d: %s\n", i++, er++); }
A kimenet a következő:
    1: 3 férfi és 2 fiu 5 disznót ettek.
    2: 2 fiu 5 disznót ettek.
    3: 5 disznót ettek.
char *strset(char *string, int c); char *strnset(char *string, int c, size_t n); void *memset(void *string, int c, size_t n);
A memset feltölti string első n bájtját c karakterrel, és visszaadja string kezdőcímét.
A legtöbb fejlesztő rendszerben létező, nem ANSI szabványos strset és strnset a string minden karakterét - a lánczáró zérus kivételével - c karakterre állítják át, és visszaadják a string kezdőcímét. Nincs hibát jelző visszatérési érték. Az strnset azonban a string legfeljebb első n karakterét inicializálja c-re.
size_t strspn(const char *string, const char *strCharSet); size_t strcspn(const char *string, const char *strCharSet);
Az strspn annak a string elején levő, maximális alkarakterláncnak a méretét szolgáltatja, mely teljes egészében csak az strCharSet-beli karakterekből áll. Ha a string nem strCharSet-beli karakterrel kezdődik, akkor zérust kapunk. Nincs hibát jelző visszatérési érték. A keresésbe nem értendők bele a lánczáró zérus karakterek.
Az strcspn viszont annak a string elején levő, maximális alkarakterláncnak a méretét szolgáltatja, mely egyetlen karaktert sem tartalmaz az strCharSet-ben megadott karakterekből. Ha a string strCharSet-beli karakterrel kezdődik, akkor zérust kapunk. Például a:
    #include <string.h>
    #include <stdio.h>
    void main(void){
      char string[] = "xyzabc";
      printf("%s láncban az első a, b vagy c indexe %d\n",
             string, strcspn(string, "abc")); }
eredménye:
    xyzabc láncban az első a, b vagy c indexe 3
char *strstr(const char *string1, const char *string2);
A függvény string2 karakterlánc első előfordulásának címét szolgáltatják string1-ben, ill. NULL mutatót kapunk, ha string2 nincs meg string1-ben. Ha string2 üres karakterlánc, akkor a rutin string1-gyel tér vissza. A keresésbe nem értendők bele a lánczáró zérus karakterek.
char *strtok(char *strToken, const char *strDelimit);
A függvény a következőleg megtalált, strToken-beli szimbólum címével tér vissza, ill. NULL mutatóval, ha nincs már további szimbólum az strToken karakterláncban. Mindenegyes hívás módosítja az strToken karakterláncot, úgy, hogy '\0' karaktert tesz a bekövetkezett elválasztójel helyére. Az strDelimit karakterlánc az strToken-beli szimbólumok lehetséges elválasztó karaktereit tartalmazza.
Az első strtok hívás átlépi a vezető elválasztójeleket, visszatér az strToken-beli első szimbólum címével, és ezelőtt a szimbólumot '\0' karakterrel zárja. Az strToken maradék része további szimbólumokra bontható újabb strtok hívásokkal. Az strToken következő szimbólumát az strToken paraméter helyén NULL mutatós strtok hívással lehet elérni. A NULL mutató első paraméter hatására az strtok megkeresi a következő szimbólumot a módosított strToken-ben. A lehetséges elválasztójeleket tartalmazó strDelimit paraméter, s így maguk az elválasztó karakterek is, változhatnak hívásról-hívásra.
A példaprogramban ciklusban hívjuk az strtok függvényt, hogy megjelentethessük az s karakterlánc összes szóközzel, vesszővel stb. elválasztott szimbólumát:
    #include <string.h>
    #include <stdio.h>
    void main( void ){
      char s[] = "Szimbólumok\tkarakterlánca\n ,, és néhány"
                 " további szimbólum";
      char selv[] = " ,\t\n", *token;
      printf("%s\nSzimbólumok:\n", s);
      /* Míg vannak szimbólumok a karakterláncban, */
      token = strtok(string, selv);
      while(token != NULL){
        /* addig megjelentetjük őket, és */
        printf("\t%s\n", token);
        /* vesszük a következőket: */
        token=strtok(NULL, selv); } }
A kimenet a következő:
    Szimbólumok karakterlánca
    ,, és néhány további szimbólum
    Szimbólumok:
    Szimbólumok
    karakterlánca
    és
    néhány
    további
    szimbólum
Mutatótömbök
Mutatótömb a
típus *azonosító[<konstans-kifejezés>] <={inicializátorlista}>;
deklarációval hozható létre. Az azonosító a mutatótömb neve: konstans mutató, melynek típusa (típus **), vagyis típus típusú objektumra mutató mutatót címző mutató. Tehát olyan konstans mutató, mely (típus *) mutatóra mutat. (Rekurzívan használtuk a * operátort.) A tömb egy eleme viszont (típus *) típusú változó mutató.
Kezdjünk el egy példát magyar kártyával!
A színeket kódoljuk a következőképp: makk (0), piros (1), tök (2) és zöld (3). A kártyákat egy színen belül jelöljük így: hetes (0), nyolcas (1), kilences (2), tízes (3), alsó (4), felső (5), király (6) és ász (7). A konkrét kártya kódja úgy keletkezik, hogy a színkód után leírjuk a kártya kódját. Ilyen alapon a 17 például piros ász.
Készítsünk char *KartyaNev(int kod) függvényt, mely visszaadja a paraméter kodú kártya megnevezését!
    /* KARTYA.H fejfajl. */
    /* Szimbolikus allandok: */
    #define SZINDB 4 /* Szinek szama. */
    #define KTYDB  8 /* Kartyak szama egy szinen belul. */
    #define OSZTO 10 /* Szin es kartyakod szetvalasztasahoz*/
    #define PAKLI SZINDB*KTYDB /* Kartyaszam a pakliban. */
    #define MAXKOD (SZINDB-1)*OSZTO+KTYDB /* Max. kartyakod.*/
    /* Prototipusok: */
    char * KartyaNev(int);
    void UjPakli(void);
    int Mennyi(void);
    int UjLap(void);

    /* KARTYA.C: Adat es fuggvenydefiniciok. */
    #include <string.h>
    #include <stdlib.h>
    #include <time.h>
    #include "KARTYA.H"
    static char *szin[SZINDB]={
      "makk ", "piros ", "tok ", "zold "};
    static char *kartya[KTYDB]={
      "VII", "VIII", "IX", "X", "also", "felso", "kiraly", "asz"};
    char * KartyaNev(int kod){
      static char szoveg[20];
      if(kod>=0 && kod/OSZTO<SZINDB && kod%OSZTO<KTYDB){
        strcpy(szoveg, szin[kod/OSZTO]);
        strcat(szoveg, kartya[kod%OSZTO]); }
      else strcpy(szoveg, "zoldseg");
      return szoveg; }
Vigyázzunk nagyon a típusokkal! A szin és a kartya statikus élettartamú, belső kapcsolódású, fájl hatáskörű, karakterláncokra mutatók tömbjeinek kezdeteire mutató mutató (char **) konstansok nevei. A szin[1] a piros karakterlánc kezdetének címét tartalmazó, karakteres mutatótömb elem (char *). Igazak például a következők:
    *szin  szin[0]
    **szin  *szin[0]  'm'
A szoveg statikus élettartamú karaktertömb, így nem szűnik meg a memóriafoglalása a függvényből való visszatéréskor, csak lokális hatáskörű azonosítójával nem érhető el!
Írjunk rövid, kipróbáló programot!
    /* PELDA23.C: Kartya megnevezesek kiiratasa. */
    #include <stdio.h>
    #include "kartya.h"
    void main(void){
      int i;
      printf("Magyar kartya megnevezesek rendre:\n");
      for(i=-1; i<=MAXKOD; ++i)
        printf("%-40s", KartyaNev(i)); }

| Feladatok
| 1. Írja át a pelda18.c-ben bemutatott egesze függvényt, hogy a paraméter karakterlánc formai helyességének eldöntésén kívül még a konvertált értéket is letárolja egy második paraméterben adott címen!
    /* PELDA18Y.C: Egesz szamok atlaga, minimuma, maximuma es
       rendezese. A modosult programsorokat megcsillagoztuk:
                                                   ********* */
    #include <stdio.h>
    #include <stdlib.h> /* Az atoi miatt! */
    #include <limits.h> /* INT_MIN es INT_MAX vegett! */
    #define MAX 100 /* A tomb max. elemszama. */
    #define INP 20  /* Az input puffer merete. */
    int getline(char s[],int lim) {
      int c,i;
      for(i=0; i<lim&&(c=getchar())!=EOF&&c!='\n'; ++i)s[i]=c;
      s[i]='\0';
      while(c!=EOF && c!='\n') c=getchar();
      return(i); }
    /* A decimalis szamjegyek szama maximalisan: */
    #define HSZ sizeof(int)/sizeof(short)*5
    int egesze(const char s[], int *ertek) { /* ********* */
      int i = 0, kezd;
      /* A karakterlanc eleji feher karakterek atlepese: */
      while(s[i]==' ' || s[i]=='\n' || s[i]=='\t') ++i;
      /* Az elojel atlepese: */
      if(s[i]=='+' || s[i]=='-') ++i;
      kezd=i; /* A szamjegyek itt kezdodnek. */
      /* Elore a kovetkezo nem numerikus karakterre: */
      while(s[i]>='0' && s[i]<='9' && i-kezd<HSZ) ++i;
      /* Dontes: */
      if(kezd==i || s[i]!='\n' && s[i]!='\t' &&
        s[i]!=' ' && s[i]!=0) return 0;
      else {
        *ertek=atoi(s); /* ********* */
        return 1; } }
    void rendez(int a[],int n) {
      int i, j, m, cs;
      for(i=0; i<n-1; ++i) {
        for(j=i+1, m=i; j<n; ++j) if(a[j]<a[m]) m=j;
        if(i!=m) {
          cs=a[i];
          a[i]=a[m];
          a[m]=cs; } } }
    void main(void) {
      int n=0; /* A rendezendo elemek szama. */
      char sor[INP+1]; /* Az input puffer. */
      int a[MAX]; /* A egeszeket tarolo tomb. */
      int min, max; /* Minimum, maximum. */
      double osszeg=0.; /* Az osszeg */
      int i;
      while(n<1||n>MAX) {
        printf("\nHany egesz szamot rendezunk(1-%d)? ",MAX);
        getline(sor,INP);
        egesze(sor, &n); } /* ********* */
      printf("\n\n\n\n\nKerem a rendezendo szamokat %d es +%d"
             " kozott!\n", INT_MIN, INT_MAX);
      for(i=0; i<n; ++i) {
        printf("%3d: ", i+1); /* ********* */
        if(!getline(sor,INP)||!egesze(sor,&a[i])) --i; }
      min=max=a[0];
      for(i=0; i<n; osszeg+=a[i],++i)
        if(a[i]<min) min=a[i];
        else if(a[i]>max) max=a[i];
      printf("\nA szamsorozat\tminimuma:%14d.\n"
             "\t\tmaximuma:%14d.\n"
             "\t\tatlaga: %17.2f\n",
             min, max, osszeg/n);
      printf("\nA rendezett szamok:\n");
      rendez(a, n);
      for(i=0; i<n; i++) {
        printf("%13d",a[i]);
        if(!((i+1)%6)) putchar('\n'); }
      putchar('\n'); }
| 2. Az egyszeri programozó megpróbálta áítírni a pelda20.c-beli lebege függvényt úgy, hogy a paraméter karakterlánc formai helyességének eldöntésén kívül még a konvertált értéket is letárolja egy második paraméterben adott címen. Sajnos nem sikerült neki.
    /*01*/#include <stdlib.h>
    /*02*/#include <ctype.h>
    /*03*/int lebege(const char* const s, double* ertek){
    /*04*/  const char *kezd, ment=s;
    /*05*/  /* Feher karakterek atlepese a lanc elejen: */
    /*06*/  while(isspace(*s)) ++s;
    /*07*/  /* A mantissza elojele: */
    /*08*/  if(*s=='+'||*s=='-') ++s;
    /*09*/  kezd=s; /* A szamjegyek itt kezdodnek. */
    /*10*/  /* A mantissza egesz resze: */
    /*11*/  while(isdigit(*s)) ++s;
    /*12*/  /* A mantissza tort resze: */
    /*13*/  if(*s=='.') ++s;
    /*14*/  while(isdigit(*s)) ++s;
    /*15*/  /* Nincs szamjegy, vagy csak egy . van: */
    /*16*/  if(s==kezd||kezd+1==s&&kezd=='.') return 0;
    /*17*/  /* Kitevo resz: */
    /*18*/  if(toupper(*s)=='E'){
    /*19*/    ++s;
    /*20*/    if(*s=='+'||*s=='-')++s;
    /*21*/    /* Egy szamjegynek lennie kell a kitevoben! */
    /*22*/    if(!isdigit(*s)) return 0;
    /*23*/    while(isdigit(*s)) ++s;}
    /*24*/  /* Vege: */
    /*25*/  if(isspace(*s)||!*s) {
    /*26*/      ertek = atof(ment);
    /*27*/      return 1; }
    /*28*/  else return 0; }
| Jelölje meg azokat a sorokat, amelyekkel a program helyes működésre bírható!
| /*01*/#include <stdio.h>
| /*01*/#include <math.h>
| /*03*/int lebege(const char* s, double* ertek){
| /*03*/int lebege(char* const s, double* ertek){
| /*04*/ const char *kezd, ment=*s;
| /*04*/ const char *kezd, *ment=s;
| /*04*/ const char *kezd, *ment=*s;
| /*16*/ if(*s==*kezd||kezd+1==s&&*kezd=='.') return 0;
| /*16*/ if(s==kezd||*(kezd+1)==*s&&*kezd=='.') return 0;
| /*16*/ if(s==kezd||kezd+1==s&&*kezd=='.') return 0;
| /*16*/ if(s==kezd||kezd+1==s&&*kezd=='.') return 1;
| /*26*/ *ertek = atof(*ment);
| /*26*/ ertek = atof(*ment);
| /*26*/ *ertek = atof(ment);
| /*26*/ &ertek = atof(ment);
| 3. Készítse el a getline függvénynek azt a változatát, ami tömbindexelés helyett mindenütt karakteres mutatókkal dolgozik!
    int getline(char *s, int n) { /* Sor beolvasása s-be. */
      int c; /* A hosszát adja vissza. */
      char *t=s; /* Indexelés helyett a t mutatóval
        haladunk az s karaktertömbön. A t mindig a következő
        szabad helyre mutat a tömbben, melynek túlírását az n
        elfogyása akadályozza meg. */
      while(n-->0&&(c=getchar())!=EOF&&c!='\n') *t++=c;
      *t='\0';
      while(c!=EOF&&c!='\n') c=getchar();
      /* A karakterlácot záró '\0' címéből a tömb kezdőcímét
         kivonva, éppen a karakterlánc hosszát kapjuk. */
      return(t-s); }
| 4. Programozónk megpróbálta elkészíteni azt a függvényt, amely kitörli az s karakterláncból a c karaktereket. Sajnos nem sikerült neki tökéletesen.
    /*1*/char *squeeze(char *s, int c) {
    /*2*/  char *t, *ment=s;
    /*3*/  for(t=s; *t; ++s) if(*s!=c) *t++=*s;
    /*4*/  *t='\0';
    /*5*/  return ment; }
| Jelölje meg azokat a sorokat, amelyekkel a program működőképessé tehető!
| /*1*/char *squeeze(const char *s, int c) {
| /*1*/char *squeeze(char* const s, int c) {
| /*1*/char *squeeze(const char* const s, int c) {
| /*3*/ for(t=s; *s; ++s) if(*s!=c) *t++=*s;
| /*3*/ for(t=s; *s; ++t) if(*s!=c) *s++=*t;
| /*3*/ for(t=s; *s; ++s) if(*s!=*t) *t++=*s;
| 4. A kezdő programozó megpróbált készíteni egy olyan függvényt az strtok mintájára, ami minden hívás alkalmával a t karakterlánc következő előfordulásának helyét adja vissza s-en belül. Ha már nincs több előfordulás, azt a NULL visszatérési értékkel jelzi. A függvény azonban nem lett tökéletes.
    /*1*/char *strstrnext(const char *s,const char *t) {
    /*2*/  register const char *ws;
    /*3*/  if(ws==NULL)ws=s;
    /*4*/  else ++ws;
    /*5*/  return ((char *)ws=strstr(ws,t)); }
| Jelölje meg azokat a sorokat, amelyekkel helyes működésre bírható!
| /*2*/ static const char *ws;
| /*2*/ auto const char *ws;
| /*3*/ if(ws==NULL)ws=t;
| /*3*/ if(ws)ws=s;